[アップデート] Amazon CloudWatch Synthetics で Playwright が使えるようになりました

[アップデート] Amazon CloudWatch Synthetics で Playwright が使えるようになりました

Clock Icon2024.11.28

いわさです。

Amazon CloudWatch Synthetics を使うスクリプトで定義したブラウザ操作をスケジュール実行し Web ページや API などのエンドポイントを監視することが出来ます。
このスクリプトですが、これまで Puppeteer (Node.js) あるいは Selenium Webdriver (Python) を使ってヘッドレス Chrome を実行することが出来ていました。

先日のアップデートで、新たに Playwright がサポートされるようになりました。

https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-cloudwatch-synthetics-playwright-runtime-canaries-nodejs/

Playwright は Microsoft が開発するフレームワークで DevIO でも何度か紹介されています。E2E テストで導入されている方も多いのではないでしょうか。

https://dev.classmethod.jp/articles/playwright-e2e/

Synthetics 上で Playwright が使えるランタイムは本日時点では Node.js のみですが、既存の Playwright スクリプトを流用したり、あるいはネイティブ Playwright の高度な機能を使うことが出来るようになります。
また、アナウンスでも少し触れられていますが Playwright ではマルチタブを扱うことも出来ます。

また、従来のカナリアスクリプトでは独自のログファイルを出力する方式だったのですが、Playwright の場合は CloudWatch Logs 上へログが出力されるようになっています。これによって CloudWatch Logs Insights などの機能を活用してログ分析を行うことも出来るようになっています。

ブループリントから選択

Synthetics Canaries のスクリプトエディタを確認してみるとランタイムバージョンとしてsyn-nodejs-playwright-1.0が追加されていることが確認出来ます。
Node.js のバージョンは 20.x、Playwright のバージョンは 1.44.1 です。

E5EF0B13-7673-4FBF-92A6-7F77BAE59224.png

注意点として、いくつかのブループリントでは Playwright がまだサポートされていないものがあります。

14624036-6E4D-4E71-BA6B-F59250141349.png

確認した限りではハートビートと GUI ワークフロービルダーのみが使用可能でした。
まぁハートビートあたりで初期構築出来ればどうにでも頑張れそうな気もしますが。

96342384-A2EA-44F8-9B1C-3BF25CC61EB0.png

他の Puppeteer や Selenium と違う点として設定ファイルが別で管理されています。
例えばマネジメントコンソール上の「スクリーンショットを撮る」オプションなどを変更すると、設定ファイルの以下の値が変更されます。
一方で監視先 URL はハードコーディングされていたりと、あくまでも Sysntetics 用の Playwright ライブラリで使われる共通設定のみが設定ファイルに切り出されている感じです。

D2D5A52F-D0B7-4A77-B9DB-3A0F65C2819F.png

実際に適当な Web サイトを対象に実行してみました。
ログに関しては次のように CloudWatch Logs へ出力される形になっていました。

9FB35DA5-AFDE-4A0F-A9BF-24F82D08675B.png

一方で他のフレームワークについては従来のどおりの独自テキストファイルでの出力でした。

2B141482-AB78-4E81-B9F2-CDE019E40F66.png

Lambda レイヤーを眺めてみる

Synthetics Canary は裏で Lambda 関数が作成されます。
@amzn/synthetics-playwrightにラップされているので中身の実装がわかりにくいのですが、気になる人は次のレイヤーを調べてみてください。

3074B21A-FD61-478E-9C96-98005E8AF471.png

get-layer-version-by-arnで Location を調べて Unzip しました。

% aws lambda get-layer-version-by-arn --arn arn:aws:lambda:ap-northeast-1:172291836251:layer:AWS-CW-SyntheticsNodeJsPlaywright:1
{
    "Content": {
        "Location": "https://awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com/snapshots/172291836251/AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId= ... &X-Amz-Signature=5a19fff9c4f519e8cf0627378192496b0aad83baa1160180bd0b8236ebc0e169",
        "CodeSha256": "0Fhg8YdG3Ir4C1gkXPPTebZoSCGNqZAaAZLcF2ZToz0=",
        "CodeSize": 115544034
    },
    "LayerArn": "arn:aws:lambda:ap-northeast-1:172291836251:layer:AWS-CW-SyntheticsNodeJsPlaywright",
    "LayerVersionArn": "arn:aws:lambda:ap-northeast-1:172291836251:layer:AWS-CW-SyntheticsNodeJsPlaywright:1",
    "Description": "Synthetics library for syn-nodejs-playwright-1.0 with NodeJS 20.x and Playwright 1.44.1",
    "CreatedDate": "2024-11-20T01:36:15.485+0000",
    "Version": 1,
    "CompatibleRuntimes": [
        "nodejs20.x"
    ]
}

% wget "https://awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com/snapshots/172291836251/AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId= ... &X-Amz-Signature=5a19fff9c4f519e8cf0627378192496b0aad83baa1160180bd0b8236ebc0e169"

:

Resolving awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com (awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com)... 52.219.162.106, 3.5.156.105, 52.219.8.55, ...
Connecting to awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com (awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com)|52.219.162.106|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 115544034 (110M) [application/zip]
Saving to: ‘AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId=WyDymK8tmbOWPhaJ5NNDfc2EOBWEJapc&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgH%2FIP4y69B%2FCZw+1R9’

AWS-CW-SyntheticsNodeJsPlaywright-7877 100%[===========================================================================>] 110.19M  7.35MB/s    in 15s     

2024-11-28 04:13:41 (7.50 MB/s) - ‘AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId=WyDymK8tmbOWPhaJ5NNDfc2EOBWEJapc&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgH%2FIP4y69B%2FCZw+1R9’ saved [115544034/115544034]

% unzip 'AWS-CW-SyntheticsNodeJsPlaywright-78779771-c8e6-4912-9966-d50f7074a756?versionId=WyDymK8tmbOWPhaJ5NNDfc2EOBWEJapc&X-Amz-Security-Token=IQoJb3JpZ2luX2VjEKL%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgH%2FIP4y69B%2FCZw+1R9'
:

 extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/TreeRuleObject.js  
  inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/EndpointError.js  
 extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/RuleSetObject.js  
 extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/types/ErrorRuleObject.js  
   creating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/
 extracting: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/isIpAddress.js  
   creating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/
  inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/partitions.json  
  inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/parseArn.js  
  inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/index.js  
  inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/partition.js  
  inflating: nodejs/node_modules/@aws-sdk/util-endpoints/dist-es/lib/aws/isVirtualHostableS3Bucket.js  
   creating: nodejs/node_modules/@playwright/
   creating: nodejs/node_modules/@playwright/test/
  inflating: nodejs/node_modules/@playwright/test/package.json  
  inflating: nodejs/node_modules/@playwright/test/index.js  
  inflating: nodejs/node_modules/@playwright/test/README.md  
  inflating: nodejs/node_modules/@playwright/test/index.mjs  
  inflating: nodejs/node_modules/@playwright/test/index.d.ts  
  inflating: nodejs/node_modules/@playwright/test/cli.js  
  inflating: nodejs/node_modules/@playwright/test/reporter.d.ts  
  inflating: nodejs/node_modules/@playwright/test/NOTICE  
  inflating: nodejs/node_modules/@playwright/test/LICENSE  
  inflating: nodejs/node_modules/@playwright/test/reporter.js  
  inflating: nodejs/node_modules/@playwright/test/reporter.mjs  

synthetics-playwright のpackage.jsonは次のような感じでした。
追っていってみるとsynthetics-coresynthetics-agent-clientが使われていますね。

{
  "name": "@amzn/synthetics-playwright",
  "version": "0.1.0",
  "license": "UNLICENSED",
  "repository": "ssh://git.amazon.com/pkg/SyntheticsPlaywright-NodeJsSDK",
  "homepage": "https://code.amazon.com/packages/SyntheticsPlaywright-NodeJsSDK",
  "engines": {
    "node": ">=18"
  },
  "scripts": {
    "clean": "rm -rf ./build && rm -rf ./dist && rm -rf ./node_modules && rm -rf package-lock.json || true",
    "build": "npm run lcheck && tsc",
    "watch": "tsc -w",
    "prepublishOnly": "npm run build",
    "package-artifacts": "build-tools/bin/npm-package-artifacts",
    "zip-artifacts": "build-tools/bin/npm-zip-artifacts",
    "post-npm-pretty-much": "npm run package-artifacts && npm run zip-artifacts",
    "release": "npm run build",
    "lcheck": "eslint './src/**/*.ts' --max-warnings 0",
    "lfix": "npm run -s lcheck -- --fix",
    "test": "jest --collectCoverage --collectCoverageFrom=src/__tests__/*.{ts,js}",
    "posttest": "generate-coverage-data --language typescript",
    "dev-canary": "npm run release && npm run package-artifacts && brazil-build-tool-exec sam build && brazil-build-tool-exec sam local invoke LocalDevCanary -e local-dev/events/event.json"
  },
  "main": "./dist/index.js",
  "exports": "./dist/index.js",
  "files": [
    "dist/"
  ],
  "npm-pretty-much": {
    "publishLibCommonJs": true
  },
  "dependencies": {
    "playwright": "1.44.1",
    "@playwright/test": "1.44.1",
    "@amzn/synthetics-core": "^0.1.0"
  },
  "devDependencies": {
    "@amzn/brazil": "^2.0.6",
    "@tsconfig/node18": "^18.2.4",
    "@types/jest": "^29.5.12",
    "@types/aws-lambda": "^8.10.109",
    "@types/aws-lambda-mock-context": "^3.2.0",
    "@babel/core": "^7.16.0",
    "@babel/preset-env": "^7.16.4",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.2",
    "typescript": "^5.4.5",
    "@typescript-eslint/eslint-plugin": "^6.18.1",
    "@typescript-eslint/parser": "^6.18.1",
    "eslint": "^8.56.0",
    "eslint-config-prettier": "^9.1.0",
    "eslint-config-react-app": "^7.0.1",
    "eslint-plugin-prettier": "^5.1.3"
  }
}

デフォルトで使われているメインのステップ処理部分はこんな感じになっていました。

nodejs/node_modules/@amzn/synthetics-playwright/dist/lib/synthetics-playwright.js

:

    async executeStep(stepName, func, stepConfig, pageObj) {
        // Add validation for stepName
        const stepNamePattern = /^[-a-zA-Z0-9._/]+$/;
        if (!stepNamePattern.test(stepName)) {
            throw new Error(`Invalid step name: "${stepName}". The step name can only contain letters, numbers, hyphens, underscores and periods.`);
        }
        this.logger.setStepName(stepName);
        if (!this.config) {
            this.config = await super.getConfiguration();
        }
        // Merge the passed stepConfig with the existing configuration here
        const mergedStepConfig = {
            ...this.config.stepConfiguration,
            ...(stepConfig || {}),
        };
        const currentPage = pageObj || this.page;
        const sourceURL = stepUtils.getUrl(currentPage, stepName);
        let sourceScreenshot = undefined;
        if (mergedStepConfig?.screenshotOnStepStart) {
            sourceScreenshot = await stepUtils.takeScreenShot(currentPage, stepName, constants_1.START_SUFFIX);
        }
        let stepDetails = undefined;
        stepDetails = await this.startStep(stepName, sourceURL, sourceScreenshot, stepConfig);
        try {
            const returnValue = await func();
            const destinationURL = stepUtils.getUrl(currentPage, stepName);
            await this.succeedStep(stepName, destinationURL);
            if (mergedStepConfig?.screenshotOnStepSuccess) {
                await stepUtils.takeScreenShot(currentPage, stepName, constants_1.SUCCEED_SUFFIX);
            }
            return returnValue;
        }
        catch (error) {
            const destinationURL = stepUtils.getUrl(currentPage, stepName);
            await this.failStep(stepName, stepUtils.formatError(error), destinationURL);
            if (mergedStepConfig?.screenshotOnStepFailure) {
                await stepUtils.takeScreenShot(currentPage, stepName, constants_1.FAILED_SUFFIX);
            }
            if (!mergedStepConfig?.continueOnStepFailure ||
                stepUtils.formatError(error).includes(constants_1.SUCCEED_STEP_ERROR)) {
                const stackTrace = (0, util_1.inspect)(error);
                const errorMessage = `Step ${stepDetails.stepNum}: ${stepName} failed with: ${stackTrace}`;
                throw new Error(errorMessage);
            }
        }
        finally {
            await this.endStep(stepName);
            this.logger.resetStepName();
        }
    }

:

あまり中身を意識する必要もないのですが、既存 Playwright スクリプトを Synthetics に流用しようとする際に AWS 側のレイヤーについて把握したい場合があると思うので、その際に見てみると良いのではないでしょうか。

https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Synthetics_WritingCanary_Nodejs_Playwright.html

例えばアナウンスにあったマルチタブの機能は、特に Synthetics 側で何か用意されているわけではなさそうなので、Playwright 側の Page オブジェクトを普通に複数使う感じで良さそうでした。

https://playwright.dev/docs/pages

さいごに

本日は Amazon CloudWatch Synthetics で Playwright が使えるようになったので使ってみました。

ブループリントからほぼ変えずにハートビート定期実行するくらいであればたいした恩恵はない気もしますが、ゴリゴリにスクリプトをカスタマイズするような方は Playwright に乗り換えると楽になるのではないでしょうか。ぜひ使ってみてください。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.